<?php
/**
 * The MIT License
 *
 * Copyright (c) <year> <copyright holders>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * Core del framework Bxd che attiva tutte le fuzioni base per il corretto funzionamento
 * dell'applicazione
 *
 * @author Damiano Ciarla
 * @package core
 */
class Bxd
{
	/**
	 * Nome delle classi di configurazione
	 */
	private $_runConfiguration = array
	(
		/**
		 * Configurazione dove sono presenti le route di default
		 */
		"Action",

		/**
		 * Configurazione dove è possibile modificare la cartelle dove sono contenuti i file dell'applicazione
		 */
		"Folder",

		/**
		 * Configurazione dove è possibile modificare il runtime dei plugin
		 */
		"Plugin"
	);

	/**
	 * Istanza del controller che verrà chiamato
	 *
	 * @var bxd\core\controller\AbstractController
	 */
	private $_controller;

	/**
	 * Path dove è configurata l'applicazione
	 *
	 * @var string
	 */
	public $pathApplication;

	/**
	 * Path dove è configurato il core del framework Bxd
	 *
	 * @var string
	 */
	public $pathBxd;

	/**
	 * Classe di configurazione dei path del framework e dell'applicazione
	 *
	 * @var \bxd\core\config\Folder
	 */
	private $FolderConf;

	/**
	 * Classe di configurazione delle route di default
	 *
	 * @var \bxd\core\config\Action
	 */
	private $ActionConf;

	/**
	 * Classe di configurazione per la gestione dei plugin
	 *
	 * @var \bxd\core\config\Plugin
	 */
	private $PluginConf;
	
	/**
	 *
	 * @var \bxd\core\plugin\PluginManager
	 */
	private $pluginManager;

	/**
	 * Classe contenente la route definitiva che il framework andrà a chiamare dopo
	 * il parsing della url
	 *
	 * @var \bxd\core\config\Runtime
	 */
	private $RuntimeConf;

	/**
	 * Classe che viene utilizzata per leggere i dati in qualunque punto del framework
	 *
	 * @var \bxd\core\src\Data
	 */
	public $Data;

	/**
	 * Variabile utilizzata per verificare prima di chiamare il la action del controller
	 * se è stato eseguito un precedente rerouting
	 *
	 * @var boolean
	 */
	private $_preventCallAction = false;

	/**
	 * Variabile che conterrà l'istanza di questa classe per creare il pattern SINGLETON
	 *
	 * @var Bxd
	 */
	private static $Bxd;
	
	/**
	 * Nome della cartella dove sono contenuti i file di configurazione
	 *
	 * @var string
	 */
	public $folderConfigClass = "config";
	
	/**
	 * Url della chiamata http
	 *
	 * @var string
	 */
	public $requestUri;

	/**
	 * Metodo per ricreare il pattern SINGLETON della classe principale del FRAMEWORK
	 *
	 * @return Bxd
	 */
	public static function getInstance()
	{
		if (self::$Bxd === null)
			self::$Bxd = new Bxd();

		return self::$Bxd;
	}

	private function __construct()
	{
		$this->pathApplication = realpath("../");
		$this->pathBxd = dirname(__FILE__);
		
		spl_autoload_register(array($this, "_autoloadSystemClass"));
	}

	/**
	 * Classe che attiva il framework
	 */
	public function run($folderConfigClass = null)
	{
		if($folderConfigClass !== null)
		{
			if(!is_dir($this->pathApplication . '/' . $folderConfigClass))
				throw new \bxd\core\exception\BxdException($folderConfigClass, \bxd\core\exception\BxdException::CONFIG_FOLDER_NOT_EXISTS);
			
			$this->folderConfigClass = $folderConfigClass;
		}
		
		$this->RuntimeConf = new \bxd\core\config\Runtime();

		$this->Data = new \bxd\core\src\Data();
		
		// Caricamento di tutte le configurazioni custom dell'applicazione
		$this->_loadAppConfiguration();
		
		// Caricamento della configurazione degli eventi
		$this->_loadEventConfiguration();
		
		$this->pluginManager = new bxd\core\plugin\PluginManager();
		$this->pluginManager->activeAllPlugin();

		\bxd\core\event\Manager::getInstance()->notify(
			\bxd\core\event\Notify::$ON_AFTER_LOAD_CONFIG, array()
		);

		$this->requestUri = rtrim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/');

		\bxd\core\event\Manager::getInstance()->notify(
			\bxd\core\event\Notify::$ON_BEFORE_PARSING_URL, array('url' => $this->requestUri)
		);

		// Recupero delle informazioni su controller - action - params dalla URL
		$this->_parsingUrl();
		
		\bxd\core\event\Manager::getInstance()->notify(
			\bxd\core\event\Notify::$ON_AFTER_PARSING_URL, array(
				'controller' => $this->RuntimeConf->controller,
				'action' => $this->RuntimeConf->action,
				'params' => $this->RuntimeConf->params
			)
		);

		if($this->RuntimeConf->enabledLoadController)
		{
			// Il risultato della chiamata valorizzarà la variabile che stabilisce se controller e action
			// che si stanno tentando di chiamare dall'esterno esistono o meno
			$this->RuntimeConf->err_404 = !$this->_route();

			// Se non è stato possibile eseguire la chiamata a controller/action viene verificata la presenza
			// nel file di configurazione dei valori 404 per controller e action
			if($this->RuntimeConf->err_404)
			{
				\bxd\core\event\Manager::getInstance()->notify(
					\bxd\core\event\Notify::$ON_CHECK_404_ERROR, array(
						'controller' => $this->RuntimeConf->controller,
						'action' => $this->RuntimeConf->action,
						'params' => $this->RuntimeConf->params
					)
				);

				if($this->ActionConf->controller_404 !== null && $this->ActionConf->action_404 !== null)
				{
					\bxd\core\event\Manager::getInstance()->notify(
						\bxd\core\event\Notify::$ON_BEFORE_REROUTE_TO_404_CONTROLLER_ACTION, array(
							'controller' => $this->RuntimeConf->controller,
							'action' => $this->RuntimeConf->action,
							'params' => $this->RuntimeConf->params
						)
					);

					$this->reRoute($this->ActionConf->controller_404, $this->ActionConf->action_404);
					$this->RuntimeConf->view = (($this->ActionConf->view_404 !== null) ? $this->ActionConf->view_404 : null);
				}
				else throw new \bxd\core\exception\BxdException("", \bxd\core\exception\BxdException::CONFIGURATION_NOT_SETTINGS_FOR_404);
			}
		}

		\bxd\core\event\Manager::getInstance()->notify(
			\bxd\core\event\Notify::$ON_BEFORE_LOAD_VIEW, array(
				'controller' => $this->RuntimeConf->controller,
				'action' => $this->RuntimeConf->action,
				'params' => $this->RuntimeConf->params
			)
		);

		$this->_loadView();

		\bxd\core\event\Manager::getInstance()->notify(
			\bxd\core\event\Notify::$ON_AFTER_LOAD_VIEW, array(
				'controller' => $this->RuntimeConf->controller,
				'action' => $this->RuntimeConf->action,
				'params' => $this->RuntimeConf->params
			)
		);
	}

	/**
	 * Restituisce l'istanza della configurazione delle cartelle istanziata dopo
	 * aver eseguito il run()
	 *
	 * @return bxd\core\config\Folder
	 */
	public function folderConf()
	{
		return $this->FolderConf;
	}

	/**
	 * Restituisce l'istanza della configurazione dell'applicazione istanziata dopo
	 * aver eseguito il run()
	 *
	 * @return bxd\core\config\Action
	 */
	public function actionConf()
	{
		return $this->ActionConf;
	}

	/**
	 * Restituisce l'istanza della configurazione per la gestione dei plugin istanziata
	 * dopo aver eseguito il run()
	 *
	 * @return bxd\core\config\Plugin
	 */
	public function pluginConf()
	{
		return $this->PluginConf;
	}

	/**
	 * Restituisce l'istanza della configurazione finale dell'azione su cui indirizzare
	 * la chiamata a controller e action
	 *
	 * @return bxd\core\config\Runtime
	 */
	public function runtimeConf()
	{
		return $this->RuntimeConf;
	}

	/**
	 * Caricamento della configurazione dell'applicazione
	 */
	private function _loadAppConfiguration()
	{
		foreach($this->_runConfiguration as $className)
		{
			$defaultClass = '\bxd\core\config\\' . $className;
			$varName = $className . "Conf";
			
			// Controllo la presenza della classe di configurazione.
			// Se non presente viene caricata la configurazione di default
			if(file_exists($this->pathApplication . '/' . $this->folderConfigClass . '/' . $className . '.php'))
			{
				include_once $this->pathApplication . '/' . $this->folderConfigClass . '/' . $className . '.php';

				// Se il nome della classe è corretto viene istanziata e caricata la configurazione custom dell'applicazione
				if(!class_exists($className))
					throw new \bxd\core\exception\BxdException($className, \bxd\core\exception\BxdException::CONFIG_FILES_INCORRECT_DECLARATION_CLASS_NAME);

				$folderConf = new $className();

				if(!$folderConf instanceof \bxd\core\config\InterfaceConfig)
					throw new \bxd\core\exception\BxdException($className, \bxd\core\exception\BxdException::CONFIG_FILES_NOT_EXTENDS_THE_CORRECT_CLASS);

				$this->$varName = $folderConf;
				$this->$varName->runConfiguration();
			}
			else
			{
				// Viene istanziata la classe di default per la configurazione dell'applicazione
				$this->$varName = new $defaultClass();
			}
		}
	}

	/**
	 * Caricamento della configurazione degli eventi
	 */
	private function _loadEventConfiguration()
	{
		$eventConfigFile = $this->pathApplication . '/' . $this->folderConfigClass . '/Event.php';

		if(file_exists($eventConfigFile))
		{
			include_once $eventConfigFile;

			// Se il nome della classe è corretto viene istanziata e caricata la configurazione custom dell'applicazione
			if(!class_exists("Event"))
				throw new \bxd\core\exception\BxdException("Event", \bxd\core\exception\BxdException::CONFIG_FILES_INCORRECT_DECLARATION_CLASS_NAME);

			$event = new \Event();

			if(!$event instanceof \bxd\core\event\config\InterfaceConfig)
				throw new \bxd\core\exception\BxdException("Event", \bxd\core\exception\BxdException::CONFIG_FILES_NOT_EXTENDS_THE_CORRECT_CLASS);

			$event->runConfiguration();
		}
	}

	/**
	 * Metodo che parsa la url e restituisce un array contenente il controller la action e eventuali params
	 */
	private function _parsingUrl()
	{
		if($this->RuntimeConf->controller === null && $this->RuntimeConf->action === null)
		{
			$route = explode('/', trim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/'));

			if(count($route) && !empty($route[0]))
			{
				$this->RuntimeConf->controller = $route[0];
				$this->RuntimeConf->action = ((count($route) > 1) ? $route[1] : null);

				if(count($route) > 2)
				{
					for($i = 2; $i < count($route); $i++)
						$this->RuntimeConf->params[] = $route[$i];
				}
			}
			else
			{
				$this->RuntimeConf->controller = $this->ActionConf->controller;
				$this->RuntimeConf->action = $this->ActionConf->action;
				$this->RuntimeConf->view = (($this->ActionConf->view !== null) ? $this->ActionConf->view : null);
			}
		}
	}

	/**
	 * Metodo che verifica se il file del controller esiste
	 *
	 * @param string $controller Nome del controller da caricare
	 *
	 * @return boolean
	 */
	private function _checkController()
	{
		// Path della cartella dei controller
		$controllerPath = $this->pathApplication . '/' . $this->FolderConf->folder_controller;

		// Verifico se è presente la possibilità che il controller si trovi in una sotto cartella
		if(strpos($this->RuntimeConf->controller, "_") !== false)
		{
			$controllerData = explode("_", $this->RuntimeConf->controller);

			// Ricreo la possibile la sotto cartella
			$subFolder = implode("/", array_slice($controllerData, 0, -1));

			// Imposto il controller con la prima lettera maiuscola
			$controller = ucfirst(end($controllerData));

			// Creo il path per il controller con un eventuale sotto cartella
			$pathWithSubFolder = $controllerPath . '/' . $subFolder . '/' . $controller . '.php';

			// Verifico se il controller con la sotto cartella è presente o meno
			if(file_exists($pathWithSubFolder))
			{
				$this->RuntimeConf->controller_subfolder = $subFolder;
				$this->RuntimeConf->controller = $controller;
				return true;
			}

			// Imposto il controller con la prima lettera maiuscola
			$controller = ucfirst($this->RuntimeConf->controller);

			// Creo il path classico per il controller
			$pathWithoutSubFolder = $controllerPath . '/' . $controller . '.php';

			// Verifico se questo controller esiste nell'applicazione
			if(file_exists($pathWithoutSubFolder))
			{
				$this->RuntimeConf->controller = $controller;
				return true;
			}

			return false;
		}
		else
		{
			// Imposto il controller con la prima lettera maiuscola
			$controller = ucfirst($this->RuntimeConf->controller);

			// Creo il path classico per il controller
			$pathWithoutSubFolder = $controllerPath . '/' . $controller . '.php';

			// Verifico se questo controller esiste nell'applicazione
			if(file_exists($pathWithoutSubFolder))
			{
				$this->RuntimeConf->controller = $controller;
				return true;
			}

			return false;
		}
	}

	/**
	 * Metodo che restituisce l'istanza del nuovo controller
	 *
	 * @param string $controller Nome del controller da caricare
	 *
	 * @return bxd\core\controller\AbstractController
	 */
	private function _loadController()
	{
		include_once $this->pathApplication . '/' . $this->FolderConf->folder_controller . '/' . (($this->RuntimeConf->controller_subfolder !== null) ? $this->RuntimeConf->controller_subfolder . '/' : '') . $this->RuntimeConf->controller . ".php";

		$controllerClass = $this->RuntimeConf->controller . "Controller";

		if(!class_exists($controllerClass))
			throw new \bxd\core\controller\ExceptionController($controllerClass, \bxd\core\controller\ExceptionController::INCORRECT_CLASS_NAME);

		// Istanzio il nuovo controller e ne verifico la validità dell'istanza
		$controllerInstance = new $controllerClass();

		// Controllo se l'istanza del controller è dichiarata correttamente
		if (!$controllerInstance instanceof \bxd\core\controller\AbstractController)
			throw new \bxd\core\controller\ExceptionController($controllerClass, \bxd\core\controller\ExceptionController::CONTROLLER_FILES_NOT_EXTENDS_THE_CORRECT_CLASS);

		$this->_controller = $controllerInstance;
	}

	private function _route()
	{
		\bxd\core\event\Manager::getInstance()->notify(
			\bxd\core\event\Notify::$ON_BEFORE_CHECK_CONTROLLER, array(
				'controller' => $this->RuntimeConf->controller,
				'action' => $this->RuntimeConf->action,
				'params' => $this->RuntimeConf->params
			)
		);

		// Verifico se il file del controller che si sta chiamando esiste
		if($this->_checkController())
		{
			\bxd\core\event\Manager::getInstance()->notify(
				\bxd\core\event\Notify::$ON_BEFORE_LOAD_CONTROLLER, array(
					'controller' => $this->RuntimeConf->controller,
					'action' => $this->RuntimeConf->action,
					'params' => $this->RuntimeConf->params
				)
			);

			// Eseguo il caricamento del controller
			$this->_loadController();

			\bxd\core\event\Manager::getInstance()->notify(
				\bxd\core\event\Notify::$ON_AFTER_LOAD_CONTROLLER, array(
					'controller' => $this->RuntimeConf->controller,
					'action' => $this->RuntimeConf->action,
					'params' => $this->RuntimeConf->params
				)
			);

			// Verifico se il metodo che si vuole chiamare è implementato dal controller
			if(method_exists($this->_controller, $this->RuntimeConf->action))
			{
				\bxd\core\event\Manager::getInstance()->notify(
					\bxd\core\event\Notify::$ON_BEFORE_CALL_ACTION, array(
						'controller' => $this->RuntimeConf->controller,
						'action' => $this->RuntimeConf->action,
						'params' => $this->RuntimeConf->params
					)
				);

				// Verifico che non sia stato già eseguito un reroute
				if(!$this->_preventCallAction)
					call_user_func(array($this->_controller, $this->RuntimeConf->action));

				\bxd\core\event\Manager::getInstance()->notify(
					\bxd\core\event\Notify::$ON_AFTER_CALL_ACTION, array(
						'controller' => $this->RuntimeConf->controller,
						'action' => $this->RuntimeConf->action,
						'params' => $this->RuntimeConf->params
					)
				);
			}
			else return false;
		}
		else return false;

		return true;
	}

	private function _loadView()
	{
		if($this->RuntimeConf->autocheckView)
		{
			if ($this->RuntimeConf->view === null)
				$this->RuntimeConf->view = (($this->RuntimeConf->controller_subfolder !== null) ? $this->RuntimeConf->controller_subfolder . '/' : '') . lcfirst($this->RuntimeConf->controller) . '/' . lcfirst($this->RuntimeConf->action);

			$pathView = $this->pathApplication . '/' . $this->FolderConf->folder_view . '/' . $this->RuntimeConf->view . '.php';

			if(file_exists($pathView))
			{
				$data = $this->Data;
				include_once $pathView;
			}
			else throw new \bxd\core\exception\BxdException($this->RuntimeConf->view, \bxd\core\exception\BxdException::VIEW_NOT_EXISTS);
		}
	}

	// Lista dei metodi accessibili

	/**
	 * Metodo che ritorna l'istanza del controller
	 *
	 * @return bxd\core\controller\AbstractController
	 */
	public function getController()
	{
		return $this->_controller;
	}

	/**
	 * Metodo che esegue un cambio di controller e action per reindirizzare il flusso
	 * dei dati verso un'altra destinazione
	 *
	 * @param string $controller Nome del controller
	 * @param string $action Nome della action da eseguire
	 */
	public function reRoute($controller, $action)
	{
		$this->RuntimeConf->controller = $controller;
		$this->RuntimeConf->action = $action;

		\bxd\core\event\Manager::getInstance()->notify(
			\bxd\core\event\Notify::$ON_BEFORE_REROUTE_CHECK_CONTROLLER, array(
				'controller' => $this->RuntimeConf->controller,
				'action' => $this->RuntimeConf->action,
				'params' => $this->RuntimeConf->params
			)
		);

		// Verifico se il file del controller che si sta chiamando esiste
		if($this->_checkController())
		{
			\bxd\core\event\Manager::getInstance()->notify(
				\bxd\core\event\Notify::$ON_BEFORE_REROUTE_LOAD_CONTROLLER, array(
					'controller' => $this->RuntimeConf->controller,
					'action' => $this->RuntimeConf->action,
					'params' => $this->RuntimeConf->params
				)
			);

			// Eseguo il caricamento del controller
			$this->_loadController();

			\bxd\core\event\Manager::getInstance()->notify(
				\bxd\core\event\Notify::$ON_AFTER_REROUTE_LOAD_CONTROLLER, array(
					'controller' => $this->RuntimeConf->controller,
					'action' => $this->RuntimeConf->action,
					'params' => $this->RuntimeConf->params
				)
			);

			// Verifico se il metodo che si vuole chiamare è implementato dal controller
			if(method_exists($this->_controller, $this->RuntimeConf->action))
			{
				\bxd\core\event\Manager::getInstance()->notify(
					\bxd\core\event\Notify::$ON_BEFORE_REROUTE_CALL_ACTION, array(
						'controller' => $this->RuntimeConf->controller,
						'action' => $this->RuntimeConf->action,
						'params' => $this->RuntimeConf->params
					)
				);

				call_user_func(array($this->_controller, $this->RuntimeConf->action));
				
				// Imposto questo valore a true per evitare di chiamare il controller e la action recuperati dopo il parsing dell url
				$this->_preventCallAction = true;

				\bxd\core\event\Manager::getInstance()->notify(
					\bxd\core\event\Notify::$ON_AFTER_REROUTE_CALL_ACTION, array(
						'controller' => $this->RuntimeConf->controller,
						'action' => $this->RuntimeConf->action,
						'params' => $this->RuntimeConf->params
					)
				);
			}
			else throw new \bxd\core\controller\ExceptionController(array($action, $controller), \bxd\core\controller\ExceptionController::METHOD_NOT_EXISTS);
		}
		else throw new \bxd\core\controller\ExceptionController(array($controller, \Bxd::getInstance()->folderConf()->folder_controller), \bxd\core\controller\ExceptionController::FILE_NOT_EXISTS);
	}


	/**
	 * Metodo richiamato dell'autoload per il caricamento delle classi di sistema
	 *
	 * @param string $className Nome della classe da caricare
	 */
	private function _autoloadSystemClass($className)
	{
		if(strpos($className, "\\") !== false)
		{
			$systemAutoloadClass = array("bxd","plugin","src");
			
			$classNameToPart = explode("\\", $className);

			if($classNameToPart[0] == "")
				array_shift($classNameToPart);
			
			if(in_array($classNameToPart[0], $systemAutoloadClass))
			{
				switch($classNameToPart[0])
				{
					case "bxd":				
						$classPath = implode("/", array_slice(explode("/", $this->pathBxd), 0, -1));
						break;

					case "plugin":
						$classPath = $this->pathApplication . '/' . $this->FolderConf->folder_plugin;
						break;

					case "src":
						$classPath = $this->pathApplication . '/src';
						break;
				}

				for($i = 1; $i < count($classNameToPart); $i++)
					$classPath .= '/' . $classNameToPart[$i] . (($i == count($classNameToPart) - 1) ? '.php' : '');

				if(file_exists($classPath))
				{
					include_once $classPath;
				}
				else
				{
					throw new \bxd\core\exception\BxdException($className, \bxd\core\exception\BxdException::SYSTEM_CLASS_NOT_EXISTS);
				}
			}
		}
	}
}
?>